fix(invitations): revoke/duplicate guards + resend endpoint#3857
Conversation
Revoking an accepted invitation flipped it to revoked and silently dropped it out of the accepted set used for referral attribution. The repository CAS now filters status:'pending'; the controller maps a null update on an existing invitation to 409 Conflict. The revoke-mid-claim race is benign and documented at the CAS. refs #3834
create() allowed N live tokens for the same email — any one of them opens the signup gate. Guard on the existing pending-only findByEmail (expired/revoked/accepted invites never block a re-invite) and thread the 409 through the controller instead of the hardcoded 422. refs #3834
POST /api/invitations/:invitationId/resend (admin CASL chain, also on the legacy /api/auth/invitations alias for mount consistency). Re-sends the signup-invite email with the EXISTING token — the list endpoint strips tokens, so a lost email previously meant a dead invite. 409 when not pending, 422 when the mailer is unconfigured; the send is awaited (the email IS the operation). refs #3834
Replaces a downstream consumer name in the alias-removal TODO with neutral wording (public-OSS no-downstream-refs rule). refs #3834
|
Warning Review limit reached
More reviews will be available in 13 minutes and 9 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (9)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3857 +/- ##
==========================================
+ Coverage 91.99% 92.03% +0.04%
==========================================
Files 160 160
Lines 5310 5337 +27
Branches 1708 1717 +9
==========================================
+ Hits 4885 4912 +27
Misses 337 337
Partials 88 88
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR hardens the invitations admin surface by preventing invalid state transitions (revoking accepted invites, duplicate pending invites) and adds a resend endpoint so admins can recover from failed invite email delivery without regenerating tokens.
Changes:
- Add a duplicate-pending invitation guard in
InvitationService.create()returning 409 Conflict. - Guard
InvitationRepository.revoke()to only revoke pending invites (accepted invites now refuse with 409 Conflict). - Add POST
/api/invitations/:invitationId/resend(and legacy/api/auth/invitations/:invitationId/resend) to resend the existing invite token email, with mailer/status guards.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| modules/invitations/services/invitations.service.js | Adds shared invite email payload builder, duplicate-pending guard, and resend() service method. |
| modules/invitations/repositories/invitations.repository.js | Guards revoke CAS on { _id, status: 'pending' } to prevent accepted→revoked corruption. |
| modules/invitations/controllers/invitations.controller.js | Threads 409 from create, maps revoke non-pending to 409, and adds resend controller action. |
| modules/invitations/routes/invitations.routes.js | Registers canonical resend endpoint under /api/invitations. |
| modules/auth/routes/auth.routes.js | Registers legacy alias resend endpoint under /api/auth/invitations. |
| modules/invitations/tests/invitations.service.unit.tests.js | Adds unit coverage for duplicate-pending and resend behavior/guards. |
| modules/invitations/tests/invitations.repository.unit.tests.js | Updates revoke test to assert pending-only guard. |
| modules/invitations/tests/invitations.controller.unit.tests.js | Adds controller-level coverage for 409 passthrough, revoke 409, and resend. |
| modules/invitations/tests/invitations.integration.tests.js | Adds integration coverage for revoke-accepted 409, duplicate-pending 409, and resend (canonical + alias). |
Summary
Hardens the platform-invitation ops surface (admin tab + the
/api/invitationsAPI).revoke()now filters{ _id, status: 'pending' }. Revoking an already-accepted invite (which corrupted referral attribution — it dropped out offindAccepted()used by the reconcile cron) now returns 409 Conflict ("Only pending invitations can be revoked") instead of silently flippingaccepted → revoked. A benign revoke-mid-claim race is documented.create()rejects a second pending invite for the same email with 409 ("A pending invitation already exists for this email") — previously N zombie pending invites per email were possible.POST /api/invitations/:invitationId/resend(admin CASL) re-sends the existing token (no regeneration) so a failed invite email is recoverable from the UI. 409 if the invite is non-pending, 422 if the mailer is unconfigured. Registered on the canonical mount and the legacy/api/auth/invitationsalias for consistency.Error-string casing follows the house
errors.getMessageconvention (getMessage-routed descriptions carry a trailing period; literal-string descriptions do not) — asserted at both unit and integration layers.Test plan
invitations.repository/service/controller.unit+invitations.integration— 132 tests, 7 suites green. New integration cases: revoke-accepted→409, duplicate-pending→409, resend re-sends existing token, resend guards (409 non-pending / 422 mailer-off), resend via the legacy alias.Guardrails check
npm run lintcleanauth.routes.jsdeprecation-alias comment)Closes #3834